Repository navigation
Conversation
…he socket address and TLS getters The declarations left out the value that the runtime returns when there is none, so a program that passed `tsc` read `null` or `undefined` as a number or a string. - `SyncSubprocess.exitCode` is `number | null`: `null` when a signal ended the child. - The six address properties of `Bun.Socket` and `ServerWebSocket.remoteAddress` are `T | undefined`: a Unix domain socket has no IP address, and a socket that is not connected has none to report. - `Socket.getTLSVersion()` is `string | null`. `getServername()` and `getPeerX509Certificate()` can return `undefined`. - The JSDoc of each member says when the value is absent. - The schema check no longer lists `ServerWebSocket.remoteAddress` as a known difference. - The docs copy of `SyncSubprocess` matches the declaration. - Tests that read these values as always present handle the absent value. The casts for `exitCode: null` are gone.
|
Updated 6:16 PM PT - Nov 9th, 2026
✅ @robobun, your commit f62ef0b52851f7178a89c8958e11cf175ce9717e passed in 🧪 To try this PR locally: bunx bun-pr 48411That installs a local version of the PR into your bun-48411 --bun |
|
Status: open. CI is green on How I reproduced it (Bun 1.4.4-canary.1+2596ac5d3 and 1.4.3, Linux x64). Each program below passes // 1. A signal ends the child: exitCode is null, declared number.
const r = Bun.spawnSync({ cmd: [process.execPath, "-e", "process.kill(process.pid, 'SIGKILL')"] });
console.log(r.exitCode, r.signalCode, r.exitCode > 1); // null "SIGKILL" false
// 2. A Unix domain socket: all six address properties are undefined, declared string / number.
Bun.listen({ unix: path, socket: { open(s) { console.log(s.remoteAddress, s.remotePort, s.localAddress); }, data() {} } });
await Bun.connect({ unix: path, socket: { data() {} } }); // undefined undefined undefined
// 3. A TCP WebSocket, first read in close: undefined, declared string.
Bun.serve({
port: 0,
fetch: (req, server) => (server.upgrade(req) ? undefined : new Response("no")),
websocket: { message() {}, close(ws) { ws.remoteAddress.toUpperCase(); } }, // TypeError: undefined is not an object
});Type proof: |
|
No actionable comments were generated in the recent review. WalkthroughPublic socket and subprocess declarations now represent values that may be absent. Documentation, type fixtures, and related tests were updated to reflect the nullable or optional types and their documented cases. ChangesSocket Endpoint and TLS Types
Subprocess Exit Results
Possibly related issues
Possibly related PRs
Merge Risk:
|
Problem
Bun.spawnSync().exitCodeisnullafter a signal kills the child, butbun.d.ts:8700saysnumber, soexitCode > 1is silentlyfalse.Bun.Socket(bun.d.ts:7264-7294),ServerWebSocket.remoteAddress(serve.d.ts:273) and three TLS getters have the same gap.ws.remoteAddress.toUpperCase()inclosepassestscand throwsTypeError: undefined is not an object.test/#44614 lists theexitCodegap. WebSocket: real closing handshake, one failure path,wsrewritten on native transports (consolidates the open WebSocket PRs) #45223 keepsws.remoteAddressundefinedinclose.Fix
exitCode: number | null,| undefinedfor the seven address properties,getServername()andgetPeerX509Certificate(), andgetTLSVersion(): string | null. The JSDoc says when each value is absent.node:child_processandnode:netalready read these values.test/integration/bun-types/bun-types.test.ts. The new fixture lines give 12 diagnostics onmain.Background
wsrewritten on native transports (consolidates the open WebSocket PRs) #45223 declined onegetpeernameper connection.Downsides
@types/bunget a strictNullChecks error where they read these values without a check: 8 places intest/here. Three took a signal death for a number.Bun.SocketandBun.udpSocketkeep the same gap.Notes
Runtime values, on 1.4.4-canary.1+2596ac5d3 (Linux x64) and on 1.4.3
Bun.spawnSync(...).exitCodeafter SIGKILL, and aftertimeoutormaxBufferwhen the signal ends the childnullBun.listen({ unix })orBun.connect({ unix })undefinedclose, afterclose, or inconnectErrorundefinedremoteFamily,remoteAddress,remotePortinopen, when the peer wrote and reset before the acceptundefinedremotePorton a TCP socket afterclose, also when it was read inopenundefinedws.remoteAddressoverBun.serve({ unix }), and on a TCP server when the first read is incloseundefinedgetTLSVersion()on a socket without TLS, and after a TLS socket closesnullgetServername()on a socket without TLS, after close, and when the client sent no server nameundefinedgetPeerX509Certificate()on a TLS server socket whose client sent no certificate, and after closeundefinedA child that handles the signal of
timeoutcan still exit with a code:sh -c "trap 'exit 0' TERM; sleep 5 & wait"withtimeout: 200givesexitCode: 0andexitedDueToTimeout: true. So the JSDoc ofexitCodenames the signal, not the option, as the cause.Why the declaration is the side that changes
exitCode:src/runtime/api/bun/subprocess.rs:1310returnsnullunless the child exited.spawnsync-no-microtask-drain.test.ts:87andspawn-kill-signal.test.tspinnull.src/js/node/child_process.ts:588maps it to Node'sstatus: null. The table of built-ins already saysexitCode: b.f64.nullable(src/codegen/builtins/host/globals.builtins.ts:1129).string | undefinedandu16.optional(src/runtime/socket/socket_body.bind.ts:10,:69-78).src/js/node/net.ts:3137-3165depends onundefinedfor a Unix domain socket, as Node does.socket.test.ts:5135pinsundefined.ServerWebSocket.remoteAddress:src/runtime/server/server.bind.ts:159saysstring | undefined.websocket-server.test.ts:3768pins that a value read inopenis still there inclose. The schema check inbun-types.test.tslisted this row as a known difference. The row is gone.@types/nodedeclares the Node equivalents the same way:SpawnSyncReturns.status: number | null,remoteAddress: string | undefined,localAddress?: string.Members with the same gap that this PR does not change
Checked on the same canary build.
Socket#getPeerCertificate()PeerCertificatenullwithout TLS.undefinedon a TLS server whose client sent no certificate, and after closeSocket#getCertificate()PeerCertificate | object | nullundefinedwithout TLS, on a TLS client without a certificate, and after closeSocket#getCipher()CipherNameAndProtocolundefinedwithout TLS and after closeSocket#getSharedSigalgs()string[]undefinedwithout TLS,nullafter closeSocket#exportKeyingMaterial()Bufferand asvoidundefinedwithout TLS and after closeSocket#upgradeTLS()[raw, tls]undefinedon a socket that has TLS alreadyudp.Socket#port,#address,udp.ConnectedSocket#remoteAddressnumber,SocketAddress,SocketAddressundefinedwhen the first read is afterclose()exportKeyingMaterial()needs its two declarations merged first. The UDP values are absent only after the program itself calledclose().test/#44614 have open PRs:SyncSubprocess.stdout/stderr(bun-types: type Subprocess stdio properties by what the runtime exposes for each stdio option #39283) andBun.YAML.stringify(types: fix YAML.stringify return type #34162, Implement the replacer argument of YAML, TOML and JSON5 stringify #39925).RedisClient#onconnectand#onclosereadundefinedwhen unset and are declared| null. That one is a runtime question: redis: readnullfrom an unsetonconnect/onclose, and keep only a function #48431.src/js/private.d.ts:449repeatsreadonly remoteAddress: stringfor the built-in modules, andsrc/js/node/child_process.ts:588still retypesexitCode,stdoutandstderr. Both are internal annotations and still type-check. This PR has no file undersrc/.The schema check
socket_body.bind.tsmaps tonull, so it never comparedTCPSocketandTLSSocketwithBun.Socket.robobun/e8bf2afd/schema-check-instance-classes(03be80c). It changescompare(), which bindgen: deleteb.anyand.variadic#46789 also changes, and its expected rows print type names of@types/node, which the fixture installs atlatest. It is not in this PR.Checks run
bun test test/integration/bun-types/bun-types.test.ts: 30 pass. With the two.d.tsfiles ofmain: the fixture case fails with 12 TS2344 diagnostics (spawn.ts:192-193,tcp.ts:177-185,websocket.ts:357) and the schema check prints theServerWebSocketrow.tsc --noEmitintest/: the declarations give 8 new diagnostics (3 inbun-connect-x509.test.ts, and one each incompile-trim-features.ts,react-compiler/expected.ts,host-export-callers.test.ts,socket-dns-error.test.ts,websocket-server.test.ts). After the test edits there are 0. The five projects that the Lint workflow type-checks: 0 new.host-export-callers.test.tstreated a signal death ofgit grepas "no match" (null > 1isfalse). It now throws.spawn-kill-signal.test.ts,spawn-maxbuf.test.ts,socket-dns-error.test.ts,websocket-server.test.ts -t remoteAddress,compile-trim-runtime.test.ts,bun-connect-x509.test.ts(the test that needsexample.comfails here without network, as onmain),test/aot/builtins(the same result asmain) andcargo test -p bun_aot --test ir_tests lower..d.tsfiles grow by 3203 bytes of JSDoc. Without comments, as the binary embeds them, they grow by 122 bytes, or 4 bytes after zstd (the build's ownwithoutCommentsand compression settings).InternalSocket::Pipe,src/uws_sys/socket.rs:698-726), but I did not run Windows.